5  NumPy 数组的创建与访问

5.1 引言NumPy在金融计算中的核心地位

NumPy(Numerical Python)是Python科学计算的基础包,提供了高性能的多维数组对象和相关工具。在金融数据分析中,NumPy的地位无可替代,它不仅是Pandas、Matplotlib等库的基础,其本身也提供了强大的数值计算能力。

5.2 本章学习目标

通过本章学习,你将能够:

  1. 使用 np.array() 从列表与嵌套列表创建一维、二维股价数组,并用 shapendimsizedtype 查看数组属性
  2. 使用 [start:end:step] 切片、负数索引与布尔索引访问一维数组,用 array[row, col]: 索引二维数组
  3. 使用 np.concatenate()(及 vstack/hstack)按行或按列拼接数组,用 hsplit/vsplit 分割数组
  4. 区分数组的视图与副本,安全使用 .copy()reshape().T 完成形状变换
  5. 借助广播机制完成标量折扣、列向量调整与收益率标准化,并说明向量化运算相对 Python 循环的性能优势

先修:章节 1(第1章 常用基本数据类型)、章节 2(第2章 列表、字典和元组)

5.3 数组的创建

理论背景:数组与列表的本质区别

在动手创建第一个数组之前,需要先明确NumPy数组与第2章学习的Python列表有什么根本性差异。从数据结构的角度来看,两者的对比如下:

特性 Python列表 NumPy数组
内存存储 分散存储对象引用 连续内存存储
元素类型 可以不同 必须相同
性能 解释执行,较慢 编译优化,极快
向量化 不支持 原生支持
内存效率 较低(每个元素8字节+对象开销) 较高(紧凑存储)

5.3.1 基础创建方法

NumPy提供了多种创建数组的方法:

任务要求:已知股票1、股票2 各 5 个交易日的收盘价数组 stock1stock2,依次输出三项内容:股票1数组的形状 stock1.shape、股票2周二(索引 1)的股价、两数组按 2×5 方式沿 axis=0 拼接后的矩阵 stock_con。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 5.1: 平台原始代码
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
# 已知股价数据
stock1=np.array([22.91,22.89,23.38,23.09,22.90])
stock2=np.array([41.14,40.99,41.29,40.81,40.97])  # 创建NumPy数组stock2
# 1. 查看股票1的属性值情况,并输出。
print(stock1.shape)
# 2. 输出股票2周二的股价
print(stock2[1])
# 3. 连接股票1与股票2的数据(按照2X5的方式连接),并输出
stock_con=np.concatenate(([stock1],[stock2]),axis=0)
print(stock_con)  # 输出两只股票收盘价合并后的2×5矩阵

预期输出(本机 Python 实际运行结果,具体以平台运行结果为准):

(5,)
40.99
[[22.91 22.89 23.38 23.09 22.9 ]
 [41.14 40.99 41.29 40.81 40.97]]

判读要点:(5,) 是含一个元素的元组,表示一维数组有 5 个元素,注意它与 (1, 5)(5, 1) 的区别;拼接结果为 2 行 5 列的二维数组,第 1 行是 stock1、第 2 行是 stock2

代码深度解析:

  1. np.array()的参数:

    np.array(object, dtype=None, copy=True, order=None, ndmin=0)
    • object: 列表、元组或其他数组
    • dtype: 指定数据类型(如np.float64, np.int32)
    • copy: 是否复制数据
  2. 数据类型的金融意义:

    • float32: 节省内存,适合大规模数据
    • float64: 双精度,金融计算推荐
    • int64: 整数(如成交量、日期)
  3. 内存布局:

    • C顺序(C-style): 行优先(默认)
    • F顺序(Fortran-style): 列优先

5.3.2 二维数组的创建

列表 5.2: 二维NumPy数组的创建与应用
# ==================== 创建二维数组 ====================
# np.array()可以从嵌套列表创建二维数组
# 外层列表包含2个元素(2行)
# 每个元素是一个包含5个数值的列表(5列)
# 第一行存储股票1的5日收盘价
# 第二行存储股票2的5日收盘价
stocks_2d = np.array([  # 创建二维数组(2行5列)
    [22.91, 22.89, 23.38, 23.09, 22.90],  # 第1行:股票1的收盘价
    [41.14, 40.99, 41.29, 40.81, 40.97]   # 第2行:股票2的收盘价
])

# ==================== 查看数组形状 ====================
# .shape属性返回数组的维度
# 对于二维数组,返回(行数, 列数)
print("二维数组形状:", stocks_2d.shape)  # 输出:(2, 5)

# .shape[0]访问第一个维度(行数)
print("行数:", stocks_2d.shape[0])  # 输出:2

# .shape[1]访问第二个维度(列数)
print("列数:", stocks_2d.shape[1])  # 输出:5

# ==================== 查看数组属性 ====================
# .ndim属性返回数组的维度数(秩)
# 2表示这是一个二维数组
print(f"\n数组维度: {stocks_2d.ndim}")  # 输出:2

# .size属性返回数组中元素的总数
# 2×5=10个元素
print(f"数组大小: {stocks_2d.size}")  # 输出:10

# .dtype属性返回数组的数据类型
# float64表示64位浮点数
print(f"数据类型: {stocks_2d.dtype}")  # 输出:float64

补充说明:shape属性的理解

shape属性返回一个元组,描述数组的维度:

  • 一维数组(5,): 5个元素
  • 二维数组(2, 5): 2行5列
  • 三维数组(2, 3, 4): 2个3×4的矩阵

在金融应用中:

  • 一维: 单只股票的时间序列
  • 二维: 多只股票的时间序列
  • 三维: 多个市场、多只股票、多日数据(面板数据)

5.4 数组的访问与切片

5.4.1 一维数组的索引

列表 5.3: 一维NumPy数组的索引访问
# ==================== 任务2:输出股票2周二的股价 ====================
# 假设索引0=周一,1=周二,2=周三,3=周四,4=周五
# 方括号[]用于索引访问
# stock2[1]访问数组中索引为1的元素(第2个元素)
tuesday_price_stock2 = stock2[1]  # 获取索引1的元素(周二股价)
# f-string格式化输出,.2f保留2位小数
print(f"股票2周二股价: {tuesday_price_stock2:.2f}元")  # 输出:40.99元

# ==================== 多种索引方式 ====================
# 切片语法:[start:end:step]
# start是起始索引(包含)
# end是结束索引(不包含)
# step是步长
# stock2[0:3]获取索引0,1,2的元素(前3个)
print(f"\n股票2第1-3天: {stock2[0:3]}")  # 输出前3天价格

# 负数索引从末尾开始计数
# -1是最后一个元素,-2是倒数第2个
# stock2[-2:]从倒数第2个到末尾
print(f"股票2最后2天: {stock2[-2:]}")  # 输出后2天价格

# 步长为2,每隔一个取一个元素
# stock2[::2]取索引0,2,4的元素
print(f"股票2偶数天: {stock2[::2]}")  # 输出奇数日的价格(索引0,2,4)

# ==================== 布尔索引(高级用法) ====================
# stock2 > 41.0返回布尔数组
# 比较运算符>逐元素比较
# 结果是[False, False, True, False, False]
# 使用布尔数组作为索引,返回True对应的元素
high_price_days = stock2[stock2 > 41.0]  # 筛选价格大于41的元素
print(f"\n股价>41元的交易日: {high_price_days}")  # 输出:[41.29]

索引规则总结

  • [start:end:step] start包含,end不包含
  • 负数索引从末尾计数 -1是最后一个元素
  • 省略start默认从0开始,省略end到末尾
  • ::step[:​:-1]用于反向

5.4.2 二维数组的索引

列表 5.4: 二维NumPy数组的多维索引
# ==================== 获取单个元素 ====================
# 二维数组索引语法:array[row, col]
# row是行索引,col是列索引
# stocks_2d[0, 2]表示第1行(索引0),第3列(索引2)
element = stocks_2d[0, 2]  # 获取第1行第3列的元素
print(f"stocks_2d[0, 2] = {element}")  # 输出:23.38

# ==================== 获取整行 ====================
# :表示选择该维度的所有元素
# stocks_2d[1, :]表示第2行(索引1)的所有列
row_1 = stocks_2d[1, :]  # 获取第2行的所有元素
print(f"\n股票2所有交易日价格: {row_1}")  # 输出第2行数据

# ==================== 获取整列 ====================
# stocks_2d[:, 2]表示所有行的第3列(索引2)
# :表示选择所有行
col_3 = stocks_2d[:, 2]  # 获取第3列的所有元素
print(f"周三所有股票价格: {col_3}")  # 输出第3列数据

# ==================== 切片:获取子矩阵 ====================
# stocks_2d[:, 1:4]表示所有行,第2-4列
# 1:4表示列索引1,2,3(不包含4)
# 结果是一个2×3的子矩阵
sub_matrix = stocks_2d[:, 1:4]  # 获取所有行的第2-4列
print(f"\n周二到周四的价格:\n{sub_matrix}")  # 输出子矩阵

金融应用场景:

  • 单只股票时间序列: arr[stock_id, :]
  • 单日所有股票: arr[:, day_id]
  • 特定时间段: arr[:, start:end]

5.5 数组的拼接与分割

5.5.1 concatenate()数组拼接

列表 5.5: 使用concatenate拼接NumPy数组
# ==================== 任务3:连接股票1与股票2 ====================
# np.concatenate()沿指定轴连接数组
# 参数1:([stock1], [stock2])是要连接的数组序列
# 注意:[stock1]和[stock2]必须包装在列表中
# axis=0指定沿第0轴(行方向)拼接
stock_con = np.concatenate(([stock1], [stock2]), axis=0)  # 垂直拼接两个一维数组

# ==================== 输出拼接结果 ====================
print("拼接后的数组:")  # 输出标题
print(stock_con)  # 输出拼接后的2×5数组
print(f"形状: {stock_con.shape}")  # 输出:(2, 5)

# ==================== 理解axis参数 ====================
# axis=0:沿行方向拼接(垂直堆叠)
# axis=1:沿列方向拼接(水平连接)

# ==================== 水平拼接示例 ====================
# 创建新的价格数据
# prices_aug包含2个新价格
prices_aug = np.array([22.95, 23.02])  # 待追加的价格数据

# np.concatenate()沿axis=0拼接
# 将stock2和prices_aug首尾相接
# 结果是包含7个元素的一维数组
stock2_aug = np.concatenate((stock2, prices_aug), axis=0)  # 拼接数组
print(f"\n扩充后的股票2: {stock2_aug}")  # 输出拼接后的数组

# ==================== 二维数组的拼接 ====================
# np.vstack()垂直堆叠(沿行方向)
# 等价于np.concatenate(..., axis=0)
# 将stock1和stock2作为两行堆叠
stock_vstack = np.vstack((stock1, stock2))  # 垂直堆叠

# np.hstack()水平连接(沿列方向)
# 等价于np.concatenate(..., axis=1)
# 将stock1和stock2首尾相接成一行
stock_hstack = np.hstack((stock1, stock2))  # 水平连接

# ==================== 输出拼接结果 ====================
print(f"\n垂直堆叠:\n{stock_vstack}")  # 输出2×5数组
print(f"水平连接:\n{stock_hstack}")  # 输出包含10个元素的一维数组

拼接函数对比:

函数 功能 axis参数 等价操作
concatenate 通用拼接 指定轴 -
vstack 垂直堆叠 固定0 concatenate(..., axis=0)
hstack 水平连接 固定1 concatenate(..., axis=1)
stack 创建新维度 新增 -

5.5.2 数组的分割

列表 5.6: NumPy数组的分割操作
# ==================== 水平分割(按列) ====================
# np.hsplit()水平分割数组(按列切分)
# stocks_2d是要分割的二维数组
# [2, 4]指定分割位置(在索引2和4之后分割)
# 结果是3个数组:列0-1,列2-3,列4
stocks_split = np.hsplit(stocks_2d, [2, 4])  # 在第2和第4列后分割

# ==================== 输出分割结果 ====================
print("按列分割结果:")  # 输出标题
# enumerate()遍历分割后的数组列表
# i是索引,part是分割后的数组部分
for i, part in enumerate(stocks_split):  # 遍历每个部分
    # .shape获取数组形状
    print(f"  部分{i+1}: {part.shape}")  # 输出每部分的形状

# ==================== 垂直分割(按行) ====================
# np.vsplit()垂直分割数组(按行切分)
# stocks_2d是要分割的二维数组
# 参数2表示分成2行
# 结果是2个数组,每行一个
stocks_vsplit = np.vsplit(stocks_2d, 2)  # 分成2行

# ==================== 输出分割结果 ====================
print(f"\n按行分割:")  # 输出标题
# enumerate()遍历分割后的数组列表
for i, part in enumerate(stocks_vsplit):  # 遍历每一行
    print(f"  部分{i+1}: {part}")  # 输出每行的内容

金融应用:

  • 分割训练/测试集: 时间序列数据的分割
  • 按市场分组: 沪深股票数据分离
  • 按时间段: 日内/隔夜数据分离

5.6 数组的视图与副本

关键概念:理解视图(View)和副本(Copy)的区别对避免意外数据修改至关重要。

列表 5.7: NumPy数组的视图与副本机制
# ==================== 创建原始数组 ====================
# np.array()创建一个包含5个元素的数组
original = np.array([1, 2, 3, 4, 5])  # 原始数组

# ==================== 切片返回视图 ====================
# 切片操作[1:4]返回视图(View)
# 视图共享原始数组的内存
# 修改视图会影响原数组
view = original[1:4]  # 创建切片视图
# 修改视图的第1个元素
view[0] = 999  # 修改视图的元素
# 观察到原数组也被修改
print(f"修改视图后,原数组: {original}")  # 输出:[1, 999, 3, 4, 5]

# ==================== 创建副本 ====================
# .copy()方法创建数组的副本(Copy)
# 副本拥有独立的内存
# 修改副本不影响原数组
copy = original[1:4].copy()  # 创建副本
# 修改副本的第1个元素
copy[0] = 888  # 修改副本的元素
# 原数组不受影响
print(f"修改副本后,原数组: {original}")  # 输出:[1, 999, 3, 4, 5]

# ==================== 重置原数组 ====================
# 重新赋值创建新数组
original = np.array([1, 2, 3, 4, 5])  # 重置为原始值

# ==================== Fancy Indexing ====================
# 使用列表作为索引称为Fancy Indexing
# [[0, 2, 4]]指定要访问的索引位置
# Fancy Indexing返回副本,不共享内存
fancy = original[[0, 2, 4]]  # Fancy Indexing创建副本
# 修改副本的第1个元素
fancy[0] = 777  # 修改副本的元素
# 原数组不受影响
print(f"Fancy索引修改副本,原数组: {original}")  # 输出:[1, 2, 3, 4, 5]

规则总结:

  • 切片 → 视图(共享内存)
  • Fancy索引 → 副本(独立内存)
  • 布尔索引 → 副本
  • .copy() → 强制副本

最佳实践: 当不确定时,使用.copy()创建副本以避免意外修改。

5.7 数组的形状操作

列表 5.8: NumPy数组的形状变换
# ==================== 创建一维数组 ====================
# np.arange()创建0-9的一维数组
# 包含10个元素
arr_1d = np.arange(10)  # 创建一维数组
print(f"一维数组: {arr_1d}")  # 输出:[0 1 2 3 4 5 6 7 8 9]

# ==================== reshape改变形状 ====================
# .reshape()改变数组的形状而不改变数据
# (2, 5)表示转换为2行5列的二维数组
# 元素总数必须保持不变(10=2×5)
arr_2x5 = arr_1d.reshape(2, 5)  # 转换为2×5数组
print(f"\n2×5数组:\n{arr_2x5}")  # 输出二维数组

# ==================== reshape(-1)自动计算维度 ====================
# -1表示自动计算该维度的大小
# reshape(-1, 2)表示自动计算行数
# 10个元素,每行2列,自动计算为5行
arr_5x2 = arr_1d.reshape(-1, 2)  # 自动转换为5×2数组
print(f"\n5×2数组:\n{arr_5x2}")  # 输出5×2数组

# ==================== ravel和flatten(展平数组) ====================
# .ravel()将多维数组展平为一维
# 返回视图(可能),不保证创建副本
flat_ravel = arr_2x5.ravel()  # 展平为数组(可能是视图)

# .flatten()将多维数组展平为一维
# 返回副本,总是创建新数组
flat_flatten = arr_2x5.flatten()  # 展平为数组(总是副本)

# ==================== 输出展平结果 ====================
print(f"\nravel结果: {flat_ravel}")  # 输出展平后的数组
print(f"flatten结果: {flat_flatten}")  # 输出展平后的数组

# ==================== 转置 ====================
# .T属性返回数组的转置
# 行列互换:2×5变为5×2
transpose = arr_2x5.T  # 转置数组
print(f"\n转置后({transpose.shape}):\n{transpose}")  # 输出转置后的数组

5.8 广播(Broadcasting)机制

广播是NumPy的强大特性,允许不同形状的数组进行算术运算。

列表 5.9: NumPy广播机制的应用
# ==================== 场景1:标量与数组 ====================
# prices是包含5个价格的一维数组
prices = np.array([10, 20, 30, 40, 50])  # 价格数组

# discount是标量(单个数值)
discount = 0.9  # 打9折的折扣率

# 标量与数组运算时,标量自动"广播"到每个元素
# 相当于[10*0.9, 20*0.9, 30*0.9, 40*0.9, 50*0.9]
discounted_prices = prices * discount  # 广播乘法
print(f"折扣后价格: {discounted_prices}")  # 输出折扣后价格

# ==================== 场景2:不同形状的数组 ====================
# base_prices是3只股票的基准价格
base_prices = np.array([10, 20, 30])  # 3只股票价格(一维)

# adjustments是3×1的二维数组(列向量)
# 代表3种不同的调整情景
adjustments = np.array([[1.1], [0.9], [1.0]])  # 调整系数(列向量)

# 广播机制:
# base_prices形状(3,)广播为(3, 3)
# adjustments形状(3, 1)广播为(3, 3)
# 结果是3×3矩阵
result = base_prices * adjustments  # 广播计算
print(f"\n调整后的价格矩阵:\n{result}")  # 输出3×3矩阵

# ==================== 场景3:标准化收益率 ====================
# returns是2×3的收益率数组
# 2只股票,3个时间点
returns = np.array([[0.05, 0.03, 0.04], [0.06, 0.02, 0.05]])  # 收益率矩阵

# 沿axis=1计算每行的均值
# keepdims=True保持维度(结果为2×1而非(2,))
mean = returns.mean(axis=1, keepdims=True)  # 计算均值并保持维度

# 沿axis=1计算每行的标准差
# keepdims=True保持维度
std = returns.std(axis=1, keepdims=True)  # 计算标准差并保持维度

# 标准化:(收益率-均值)/标准差
# 广播机制使mean和std自动扩展到与returns相同的形状
normalized = (returns - mean) / std  # 标准化计算
print(f"\n标准化后的收益率:\n{normalized}")  # 输出标准化后的数据

广播规则:

  1. 从尾部(最右边)开始比较维度
  2. 维度相同或其中一个为1则兼容
  3. 缺失的维度视为1

金融应用:

  • 批量调整: 对多只股票应用统一系数
  • 风险调整: 按波动率调整收益率
  • 组合优化: 权重向量与收益率矩阵相乘

5.9 性能优化向量化 vs 循环

为什么NumPy更快?

  1. 连续内存: 缓存友好,减少内存访问延迟
  2. 类型一致: 避免动态类型检查
  3. 向量化操作: 底层C/Fortran实现,利用SIMD指令
  4. 广播机制: 自动对齐不同形状的数组

下面的实验用同样的计算量对比两种写法的耗时:

列表 5.10: 向量化操作的性能优势
# ==================== 导入必要的库 ====================
# import time导入时间模块,用于性能测试
import time  # 导入时间模块
np.random.seed(42)  # 固定随机种子,保证每次运行生成相同的模拟数据

# ==================== 生成大数据集 ====================
# n定义数据规模
n = 1000000  # 100万个数据点

# np.random.randn()生成标准正态分布的随机数
# * 10 + 100将数据调整为均值100,标准差10
# 模拟股价数据
prices = np.random.randn(n) * 10 + 100  # 生成模拟股价数据

# ==================== 方法1:Python循环(慢) ====================
# 记录开始时间
start = time.time()  # 获取开始时间

# 使用for循环计算收益率
# []创建空列表,用于存储结果
returns_loop = []  # 初始化空列表
# range(1, len(prices))生成1到n-1的索引
for i in range(1, len(prices)):  # 遍历每个时间点
    # 计算简单收益率:(当前价格-前一日价格)/前一日价格
    ret = (prices[i] - prices[i-1]) / prices[i-1]  # 计算收益率
    returns_loop.append(ret)  # 添加到列表
# 计算循环耗时
loop_time = time.time() - start  # 计算耗时

# ==================== 方法2:NumPy向量化(快) ====================
# 记录开始时间
start = time.time()  # 获取开始时间

# NumPy向量化计算
# prices[1:]获取第2个到最后的元素
# prices[:-1]获取第1个到倒数第2个的元素
# 数组运算自动向量化,无需显式循环
returns_vec = (prices[1:] - prices[:-1]) / prices[:-1]  # 向量化计算收益率
# 计算向量化耗时
vec_time = time.time() - start  # 计算耗时

# ==================== 输出性能对比 ====================
# f-string格式化输出,:.4f保留4位小数
print(f"循环时间: {loop_time:.4f}秒")  # 输出循环耗时
print(f"向量化时间: {vec_time:.4f}秒")  # 输出向量化耗时
# 计算性能提升倍数
print(f"性能提升: {loop_time/vec_time:.1f}倍")  # 输出加速比

5.10 本章小结

要点:

  • np.array() 从(嵌套)列表创建数组;shapendimsizedtype 描述数组的结构与元素类型
  • 切片 [start:end:step] 含头不含尾;二维索引用 array[row, col],: 可选取整行或整列
  • 布尔索引按条件筛选元素;concatenate/vstack/hstack 负责拼接,hsplit/vsplit 负责分割
  • 切片返回视图(共享内存),Fancy 索引、布尔索引与 .copy() 返回副本
  • reshape(-1, ...) 自动推算维度,.T 转置;广播从尾部维度对齐,向量化运算远快于循环

易错点:

  • (5,)(1, 5)(5, 1) 是三个不同形状,拼接或矩阵运算前应先检查 .shape
  • 修改切片结果会同时改动原数组;需要独立数据时必须显式 .copy()
  • np.concatenate(([stock1],[stock2]), axis=0) 中一维数组要先升维;直接拼接两个同长一维数组得到的是更长的一维数组
  • 切片 1:4 只取索引 1、2、3;负数索引从 -1 开始计数

5.11 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。

    import numpy as np
    a = np.array([22.91, 22.89, 23.38, 23.09, 22.90])
    print(a.shape)
    b = a[1:4]
    print(b)
    b[0] = 0.0
    print(a)
    c = a[[0, 4]]
    c[0] = 0.0
    print(a)
    print(a.reshape(5, 1).shape)

    参考答案(先写下你的预测再点开)

    解题思路:逐行推演。a 是含 5 个元素的一维数组,a.shape 输出 (5,)(注意是含一个元素的元组,不是 5)。b = a[1:4] 是切片,含头不含尾,取索引 1、2、3 的元素,得到 [22.89, 23.38, 23.09];切片返回视图,与 a 共享内存,因此 b[0] = 0.0 会把 a 中索引 1 的位置一并改掉,a 变为 [22.91, 0.0, 23.38, 23.09, 22.9]c = a[[0, 4]] 是 Fancy 索引,取索引 0、4 的元素,得到 [22.91, 22.9];Fancy 索引返回副本c[0] = 0.0 不影响 a,再次打印 a 仍是 [22.91, 0.0, 23.38, 23.09, 22.9]。最后 a.reshape(5, 1) 把 5 个元素重排成 5 行 1 列的二维数组,.shape 输出 (5, 1)。另注意 NumPy 打印数组时小数末尾的 0 会被省略,0.0 显示为 0.、22.90 显示为 22.9

    # 验证脚本:验证切片视图与Fancy索引副本的差异
    import numpy as np  # 导入NumPy库
    a = np.array([22.91, 22.89, 23.38, 23.09, 22.90])  # 创建一维股价数组
    print(a.shape)  # 输出形状(一维,5个元素)
    b = a[1:4]  # 切片取索引1、2、3,得到视图
    print(b)  # 输出切片结果
    b[0] = 0.0  # 修改视图的首个元素
    print(a)  # 原数组同步被修改(视图共享内存)
    c = a[[0, 4]]  # Fancy索引取索引0、4,得到副本
    c[0] = 0.0  # 修改副本的首个元素
    print(a)  # 原数组不受影响(副本独立)
    print(a.reshape(5, 1).shape)  # 重排为5行1列后输出形状

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    (5,)
    [22.89 23.38 23.09]
    [22.91  0.   23.38 23.09 22.9 ]
    [22.91  0.   23.38 23.09 22.9 ]
    (5, 1)

    回扣本章:对应本章小结“要点”第 4 条与“易错点”第 2 条——切片返回视图(共享内存),Fancy 索引、布尔索引与 .copy() 返回副本;修改切片结果会同时改动原数组。

  2. 概念辨析:(5,)(1, 5)(5, 1) 三种形状各有几个维度、各适合表示什么金融数据?np.concatenate((stock1, stock2))np.concatenate(([stock1], [stock2]), axis=0) 的结果有何不同?再说明:切片得到的 b 与 Fancy 索引得到的 c,修改其中元素后原数组 a 的状态为何不一样。

    参考答案(点开前请先独立完成)

    解题思路:逐点作答。第一,(5,) 是一维(ndim=1),5 个元素排成一行,适合表示单只股票 5 个交易日的时间序列;(1, 5) 是二维(ndim=2),1 行 5 列,适合表示“1 只股票 × 5 天”的矩阵行(可沿 axis=0 继续堆叠其他股票);(5, 1) 也是二维,5 行 1 列,适合作为列向量(如 5 个样本对同一资产的特征列)。三者元素个数都是 5,但形状不同,不能直接参与需要形状匹配的矩阵运算,拼接前应先检查 .shape。第二,本机实测:np.concatenate((stock1, stock2)) 把两个各含 5 个元素的一维数组首尾相接,得到长度为 10 的一维数组(形状 (10,));而 np.concatenate(([stock1], [stock2]), axis=0) 先用 [stock1][stock2] 把一维数组升维成 (1, 5) 的二维行,再沿 axis=0 逐行堆叠,得到 (2, 5) 的二维矩阵——一维数组必须先升维才能按行拼接成矩阵。第三,切片与 Fancy 索引的差别源于内存:切片 b = a[1:4] 返回视图,ba 共享同一段内存,只是 viewing 的窗口不同,改 b 就是改 a;Fancy 索引 c = a[[0, 4]] 要按任意次序取元素,无法用一段连续内存表示,NumPy 因此返回一份新开辟内存的副本,改 ca 无关。需要独立数据时应显式 .copy()

    回扣本章:对应本章小结“易错点”第 1、3 条与“要点”第 4 条——(5,)(1, 5)(5, 1) 是三个不同形状;一维数组拼接成矩阵前要先升维;切片是视图、Fancy 索引是副本。

  3. 变式任务(平台任务同型改造):已知三只股票各 5 个交易日的收盘价 s1 = np.array([22.91, 22.89, 23.38, 23.09, 22.90])s2 = np.array([41.14, 40.99, 41.29, 40.81, 40.97])s3 = np.array([10.5, 10.62, 10.58, 10.71, 10.66])。创建 3×5 的二维数组并输出其 shapedtype;输出股票2周四(索引 3)的股价;输出周三(列索引 2)三只股票的价格;再用布尔索引筛选出 s3 中大于 10.60 的价格。

    参考答案(点开前请先独立完成)

    解题思路:用 np.array([s1, s2, s3]) 把三个一维数组作为三行组成 (3, 5) 二维数组,dtype 为 float64;股票2 周四的股价直接 s2[3],得到 40.81;周三(列索引 2)三只股票的价格用整列切片 stocks_3x5[:, 2],得到 [23.38, 41.29, 10.58];布尔索引 s3[s3 > 10.60] 先由比较运算生成布尔数组,再筛选出 True 位置的价格,s3 中大于 10.60 的有 10.62、10.71、10.66 三个。

    # 变式程序:三只股票收盘价的二维数组构建与访问
    import numpy as np  # 导入NumPy库
    s1 = np.array([22.91, 22.89, 23.38, 23.09, 22.90])  # 股票1的5日收盘价
    s2 = np.array([41.14, 40.99, 41.29, 40.81, 40.97])  # 股票2的5日收盘价
    s3 = np.array([10.5, 10.62, 10.58, 10.71, 10.66])  # 股票3的5日收盘价
    stocks_3x5 = np.array([s1, s2, s3])  # 三行堆叠成3×5二维数组
    print(stocks_3x5.shape, stocks_3x5.dtype)  # 输出形状与元素类型
    print(s2[3])  # 输出股票2周四(索引3)的股价
    print(stocks_3x5[:, 2])  # 输出周三(列索引2)三只股票的价格
    print(s3[s3 > 10.60])  # 布尔索引筛选s3中大于10.60的价格

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    (3, 5) float64
    40.81
    [23.38 41.29 10.58]
    [10.62 10.71 10.66]

    注意:以上为本题变式的独立代码;列表 5.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应本章小结“要点”第 2、3 条——二维索引用 array[row, col]: 选取整列;布尔索引按条件筛选元素。

  4. 动手验证:对第 3 题得到的 3×5 数组,分别用 reshape(5, 3)reshape(-1, 1).T 变换形状并打印 .shape,观察元素总数不变的前提下行列如何重排;再用 np.hsplit 把它按列分成前 2 列与后 3 列两部分。

    参考答案(点开前请先独立完成)

    解题思路:原数组 (3, 5) 共 15 个元素,三种变换都不改变元素总数与 C 序(行优先)下的元素顺序,只改变行列划分:reshape(5, 3) 得到 (5, 3),5 行 3 列;reshape(-1, 1)-1 表示自动推算该维度,15 个元素每列 1 个,自动算出 15 行,得到 (15, 1) 的列向量;.T 转置把行列互换,得到 (5, 3)——同样显示 (5, 3),但转置是“原来的列变成行”,元素排布与 reshape(5, 3) 不同(转置第 1 行是原第 1 列的三个价格)。np.hsplit(stocks_3x5, [2]) 以索引 2 为分割点按列切分,前一部分含列 0、1(形状 (3, 2)),后一部分含列 2、3、4(形状 (3, 3))。

    # 验证脚本:形状变换与按列分割(沿用第3题的stocks_3x5数组)
    import numpy as np  # 导入NumPy库
    stocks_3x5 = np.array([[22.91, 22.89, 23.38, 23.09, 22.90],  # 三只股票收盘价矩阵
                           [41.14, 40.99, 41.29, 40.81, 40.97],
                           [10.5, 10.62, 10.58, 10.71, 10.66]])
    print(stocks_3x5.reshape(5, 3).shape)  # 重排为5行3列
    print(stocks_3x5.reshape(-1, 1).shape)  # -1自动推算为15行1列
    print(stocks_3x5.T.shape)  # 转置:行列互换
    parts = np.hsplit(stocks_3x5, [2])  # 在第2列后按列分割
    print(parts[0].shape, parts[1].shape)  # 输出两部分的形状

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    (5, 3)
    (15, 1)
    (5, 3)
    (3, 2) (3, 3)

    回扣本章:对应本章小结“要点”第 5 条——reshape(-1, ...) 自动推算维度,.T 转置,hsplit/vsplit 负责分割;元素总数不变的前提下只重排行列。